Skip to content

feat(synapse-core): add piece batcher with message-size addPieces limiter - #933

Open
hugomrdias wants to merge 2 commits into
masterfrom
hugomrdias/batching
Open

feat(synapse-core): add piece batcher with message-size addPieces limiter#933
hugomrdias wants to merge 2 commits into
masterfrom
hugomrdias/batching

Conversation

@hugomrdias

@hugomrdias hugomrdias commented Aug 21, 2026

Copy link
Copy Markdown
Member

Summary

  • Add createPieceBatcher to park/pull pieces immediately and coalesce on-chain addPieces / createDataSetAndAddPieces with a tumbling window.
  • Replace the 40-piece count cap with a Filecoin message-size limiter (addPiecesFits) and Curio PieceCID size bounds (MIN_UPLOAD_SIZE / MAX_UPLOAD_SIZE).
  • Wire the same message-size check into synapse-sdk presignForCommit / pull / commit. One-shot upload() is unchanged.

How it works

Park and pull run per piece, immediately. The tumbling window only batches the on-chain call:

upload / pull / enqueue ──► piece on this SP
                              │
                              ▼
                    tumbling addPieces window
                              │
              delay elapsed, limiter overflow,
              flush(), or close()
                              │
                              ▼
         addPieces  or  createDataSetAndAddPieces
  • Existing data set: each flush submits addPieces (tx hash + status URL; callers poll if they need confirmation).
  • No data set: first flush uses createDataSetAndAddPieces (waits until the set exists, then caches it). Later windows use addPieces.
  • Wait: default { kind: 'delay', ms: 0 } starts the timer only once the window has a piece and no upload / pull is still parking. New parking restarts the delay, so concurrent operations coalesce despite size or provider-latency differences. ms: 0 flushes on the next macrotask after parking settles. { kind: 'limiter' } sits until the next piece does not fit, or flush / close.
  • Overflow: if pending + incoming does not fit, the current window flushes and the incoming piece starts the next one. A piece that cannot sit in a batch alone is rejected.
  • Pull auth: pull signs extraData for that one piece so Curio can estimateGas. Flush signs a new extraData for the whole window.
  • Failures: a failed park/pull never enters the window (siblings continue; retry upload / pull). A failed flush rejects every slot with AddPiecesFlushError; retry with enqueue.
  • onParked: runs after the piece is on this SP and before it joins the window. Throw to keep it out of the batch.

Public interfaces

Import from @filoz/synapse-core/sp (and errors from @filoz/synapse-core/errors).

function createPieceBatcher(
  client: Client<Transport, Chain, Account>,
  options: createPieceBatcher.OptionsType
): PieceBatcher

type createPieceBatcher.OptionsType = {
  dataSet: PdpDataSet | undefined
  wait?: PieceBatcherWait              // default { kind: 'delay', ms: 0 }
  limiter?: Limiter                    // default addPiecesFits
  serviceURL?: string                  // required when dataSet is undefined
  payee?: Address                      // required when dataSet is undefined
  payer?: Address
  metadata?: MetadataObject
  cdn?: boolean
}

type PieceBatcherWait =
  | { kind: 'delay'; ms: number }
  | { kind: 'limiter' }

type PieceBatcher = {
  upload: (input: UploadInput) => Promise<PieceResult>
  pull: (input: PullInput) => Promise<PieceResult>
  enqueue: (piece: EnqueuePiece) => Promise<PieceResult>
  flush: () => Promise<FlushResult | undefined>
  close: () => Promise<void>
  readonly pending: readonly EnqueuePiece[]
  readonly dataSet: PdpDataSet | undefined
}

type UploadInput = {
  data: File | Uint8Array | ReadableStream<Uint8Array>
  size?: number                        // for streams; File uses .size
  metadata?: MetadataObject
  pieceCid?: PieceCID
  onParked?: OnParked
  onProgress?: (bytesUploaded: number) => void
  signal?: AbortSignal
}

type PullInput = {
  pieceCid: PieceCID
  sourceUrl: string
  metadata?: MetadataObject
  onParked?: OnParked
}

type EnqueuePiece = { pieceCid: PieceCID; metadata?: MetadataObject }
type FlushResult = { txHash: Hex; statusUrl: string; pieces: EnqueuePiece[] }
type PieceResult = FlushResult & { pieceCid: PieceCID; batchIndex: number }
type OnParked = (piece: EnqueuePiece) => void | Promise<void>

Limiter (also used by synapse-sdk presignForCommit / pull / commit):

function addPiecesFits(options: LimiterOptions): boolean
function assertAddPiecesFit(options: LimiterOptions): void
function assertPieceCidSize(pieceCid: PieceCID): void
function estimateAddPiecesCalldataSize(options: LimiterOptions): number

type Limiter = (options: LimiterOptions) => boolean
type LimiterOptions =
  | { kind: 'addPieces'; dataSet?: PdpDataSet; pieces: LimiterPiece[] }
  | { kind: 'createDataSetAndAddPieces'; metadata?: MetadataObject; cdn?: boolean; pieces: LimiterPiece[] }

Budget is SIZE_CONSTANTS.MAX_ADD_PIECES_MESSAGE_SIZE (64 KiB minus overhead), estimated from encoded addPieces calldata with dummy extraData. PieceCID raw size must be within Curio's MIN_UPLOAD_SIZEMAX_UPLOAD_SIZE. MAX_ADD_PIECES_BATCH_SIZE (40) remains a fee-preview heuristic only.

Examples

Existing data set — coalesced uploads

import { createPieceBatcher } from '@filoz/synapse-core/sp'

const batcher = createPieceBatcher(client, { dataSet })

const [a, b] = await Promise.all([
  batcher.upload({ data: fileA }),
  batcher.upload({ data: fileB }),
])
// a.txHash === b.txHash when they landed in the same window

await batcher.close()

Create a data set on first flush

const batcher = createPieceBatcher(client, {
  dataSet: undefined,
  serviceURL: provider.pdp.serviceURL,
  payee: provider.serviceProvider,
  payer: client.account.address,
  cdn: false,
})

await batcher.upload({ data: firstFile })
await batcher.close()
// batcher.dataSet is now the created set; later windows use addPieces

Mix upload and SP-to-SP pull

await Promise.all([
  batcher.upload({ data: localFile }),
  batcher.pull({
    pieceCid,
    sourceUrl: `https://primary.example/pdp/piece/${pieceCid}`,
  }),
])
await batcher.close()

onParked, then retry a failed flush with enqueue

import { AddPiecesFlushError } from '@filoz/synapse-core/errors'

try {
  await batcher.upload({
    data: file,
    onParked: ({ pieceCid }) => {
      // piece is on this SP; not yet in an addPieces window
    },
  })
} catch (error) {
  if (AddPiecesFlushError.is(error)) {
    await batcher.enqueue({ pieceCid: error.pieceCid, metadata: error.metadata })
  } else {
    throw error
  }
}

Limiter-only wait (tests / explicit flush)

const batcher = createPieceBatcher(client, {
  dataSet,
  wait: { kind: 'limiter' },
})

const p1 = batcher.upload({ data: fileA })
const p2 = batcher.upload({ data: fileB })
await batcher.flush() // or close()
await Promise.all([p1, p2])

Test plan

  • pnpm run lint:fix from packages/synapse-core
  • pnpm test from packages/synapse-core (857 passing)
  • Confirm delay: 0 waits for all in-flight parking and coalesces different-latency uploads
  • pnpm test in packages/synapse-sdk (storage message-size tests)
  • Confirm upload() still accepts File / Uint8Array / ReadableStream and flushes on close
  • Confirm a PieceCID below min or above max upload size is rejected before park/pull

@github-project-automation github-project-automation Bot moved this to 📌 Triage in FOC Aug 21, 2026
…iter

Park and pull immediately, then coalesce on-chain addPieces using Curio piece-size bounds and the Filecoin 64KiB message budget instead of a fixed 40-piece count.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 21, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
synapse-dev 5a20671 Commit Preview URL

Branch Preview URL
Aug 24 2026, 03:36 PM

@hugomrdias
hugomrdias force-pushed the hugomrdias/batching branch from b441f38 to 41d1ddc Compare August 21, 2026 17:59
@hugomrdias
hugomrdias marked this pull request as ready for review August 21, 2026 18:00
@hugomrdias
hugomrdias requested a review from rvagg as a code owner August 21, 2026 18:00
@hugomrdias
hugomrdias requested a review from Kubuxu August 21, 2026 18:00
return { kind: 'createDataSetAndAddPieces', metadata: datasetMetadata, cdn, pieces }
}

function fits(pieces: LimiterPiece[]): boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming for legacy datasets we are going to limit here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

@Kubuxu Kubuxu left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM, although the TS async flows are not my strong suit

@github-project-automation github-project-automation Bot moved this from 📌 Triage to ✔️ Approved by reviewer in FOC Aug 24, 2026
pull: (input: PullInput) => Promise<PieceResult>
/** Already on this SP. Join the addPieces window only. */
enqueue: (piece: EnqueuePiece) => Promise<PieceResult>
flush: () => Promise<FlushResult | undefined>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add comments for flush and close to make their expected behavior a clearer?

The current comments say that both trigger a flush and that close() waits for parking, but they don't make it clear that flush() only submits pieces that are already in windowSlots.

Comment thread packages/synapse-core/src/sp/create-piece-batcher.ts
Comment thread packages/synapse-core/src/sp/create-piece-batcher.ts
@hugomrdias hugomrdias linked an issue Aug 27, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ✔️ Approved by reviewer

Development

Successfully merging this pull request may close these issues.

Stateful batching

3 participants